// Copyright (c) Microsoft Corporation
// All rights reserved

namespace Microsoft.Windows.Controls.Specialized.Primitives {
    
    using System;
    using System.Linq;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Text;

    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;

    using Microsoft.Windows.Controls.Specialized.TextExtensions;
    using Microsoft.Windows.Controls.Specialized.TextModel;

    internal class TextLineVisualFactory {
        ITextBuffer _textBuffer;
        TextStyle _defaultStyle = TextStyle.Default;
        IClassifier _classifier;
        IDictionary<string, TextStyle> _formatMap;

        const double _maxParagraphWidth = 0;

        public TextLineVisualFactory(ITextBuffer textBuffer, IClassifier classifier, IDictionary<string, TextStyle> formatMap, TextStyle defaultStyle) {
            Debug.Assert(textBuffer != null);
            _textBuffer = textBuffer;
            _classifier = classifier;
            _formatMap = formatMap;
            if (defaultStyle != null)
                _defaultStyle = defaultStyle;
            else
                _defaultStyle = TextStyle.Default;
        }

        public TextBlock CreateTextLineVisual(int lineNumber, int length) {
            BufferLine bufferLine = _textBuffer.GetTextLine(lineNumber);
            return CreateTextLineVisual(bufferLine.TextSpan, length);
        }

        const int Max = 256;

        public TextBlock CreateTextLineVisual(TextSpan textSpan, int length) {
            IEnumerable<ClassificationSpan> classificationSpans = null;

            if (_classifier != null)
                classificationSpans = _classifier.GetClassificationSpans(textSpan);
            else
                classificationSpans = new ClassificationSpan[] { new ClassificationSpan(textSpan, "default") };

            // Actually create the text lines
            var textBlock = new TextBlock();

            // For some reason Inlines is null until Text is set... go figure.
            textBlock.Text = "";

            int end;
            if (length < 0 || length > textSpan.Length)
                if (textSpan.Length < Max)
                    end = textSpan.End;
                else
                    end = textSpan.Start + Max;
            else
                end = textSpan.Start + length;

            // If the classifications are the same, merge the spans.
            var mergedSpans = Collapse(classificationSpans, c => c.ClassificationType,
                (c1, c2) => new ClassificationSpan(MergeSpan(c1.TextSpan, c2.TextSpan), c1.ClassificationType));

            // Map the classifications to styles.
            var styledSpans = from c in mergedSpans
                         select new { TextSpan = c.TextSpan, Type = c.ClassificationType, Style = MapStyles(c) };

            // Collapse whitespace adjacent to non-whitespace if they style of the non-whitespace would produce
            // similar whitespace.
            var collapsedWhitespace = Collapse(styledSpans,
                (s1, s2) => ((s1.Type == "whitespace" || s2.Type == "whitespace") && WhiteMatch(s1.Style, s2.Style)),
                (s1, s2) => new { 
                    TextSpan = MergeSpan(s1.TextSpan, s2.TextSpan),
                    Type = s1.Type == "whitespace" ? s2.Type: s1.Type, 
                    Style = s1.Type == "whitespace" ? s2.Style : s1.Style });

            // If adjacent styles are the same, merge the styles.
            var collapsedSpans = Collapse(collapsedWhitespace, s => s.Style,
                (s1, s2) => new { TextSpan = MergeSpan(s1.TextSpan, s2.TextSpan), Type = s1.Type, Style = s1.Style });

            foreach (var span in collapsedSpans) {
                var runSpan = span.TextSpan;
                if (runSpan.Start >= end)
                    break;
                if (runSpan.End > end)
                    runSpan = new TextSpan(runSpan.TextBuffer, runSpan.Start, end - runSpan.Start);
                var run = new Run() { Text = DisplayText(SpanText(runSpan)) };
                span.Style.ApplyTo(run);

                textBlock.Inlines.Add(run);
            }
            
            textBlock.Measure(new Size(double.MaxValue, double.MaxValue));

            return textBlock;
        }

        private static IEnumerable<T> Collapse<T>(IEnumerable<T> e, Func<T, T, Boolean> match, Func<T, T, T> collapse) {
            var en = e.GetEnumerator();
            T last;
            if (en.MoveNext()) {
                last = en.Current;
                while (en.MoveNext()) {
                    if (!match(last, en.Current)) {
                        yield return last;
                        last = en.Current;
                    }
                    else
                        last = collapse(last, en.Current);
                }
                yield return last;
            }
        }

        private static IEnumerable<T> Collapse<T, K>(IEnumerable<T> e, Func<T, K> map, Func<T, T, T> collapse) where K : IEquatable<K> {
            var en = e.GetEnumerator();
            T last;
            K key;
            if (en.MoveNext()) {
                last = en.Current;
                key = map(last);
                while (en.MoveNext()) {
                    var newKey = map(en.Current);
                    if (!key.Equals(newKey)) {
                        yield return last;
                        last = en.Current;
                        key = newKey;
                    }
                    else
                        last = collapse(last, en.Current);
                }
                yield return last;
            }
        }

        private static TextSpan MergeSpan(TextSpan span1, TextSpan span2) {
            if (span1.Start < span2.Start)
                return new TextSpan(span1.TextBuffer, span1.Start, span2.End - span1.Start);
            else
                return new TextSpan(span2.TextBuffer, span2.Start, span1.End - span2.Start);
        }

        private TextStyle MapStyles(ClassificationSpan span) {
            TextStyle style;
            if (_formatMap == null || !_formatMap.TryGetValue(span.ClassificationType, out style))
                return _defaultStyle;
            return style;
        }

        // Assuming whiteStyle applies to whitespace consider the styles equal if they
        // would produce the same size whitespace.
        private bool WhiteMatch(TextStyle s1, TextStyle s2) {
            if (s1.FontFamily != s2.FontFamily) return false;
            if (s1.FontSize != s2.FontSize) return false;
            if (s1.FontStyle != s2.FontStyle) return false;
            return true;
        }

        public static string SpanText(TextSpan span) {
            var textBuffer = span.TextBuffer;

            var start = span.Start;
            var len = span.Length;
            while (len > 0) {
                switch (textBuffer.GetChar(start + len - 1)) {
                    case '\r':
                    case '\n': len--; continue;
                }
                break;
            }
            return textBuffer.GetText(start, len);
        }

        public static string DisplayText(string text) {
            if (text.IndexOf('\t') >= 0) {
                var sb = new StringBuilder();
                foreach (var c in text) {
                    switch (c) {
                        case '\t':
                            switch (sb.Length % 4) {
                                case 0: sb.Append("    "); break;
                                case 1: sb.Append("   "); break;
                                case 2: sb.Append("  "); break;
                                case 3: sb.Append(" "); break;
                            }
                            break;
                        default:
                            sb.Append(c);
                            break;
                    }
                }
                text = sb.ToString();
            }
            return text;
        }

   }


}